[iOS] Swift Chartsでグラフを表示してみる
こんにちは。きんくまです。
iOS 16からSwift Chartsが加わったので、グラフを表示してみたいです。
グラフの元データを気象庁から取得
サンプルデータとして、気象庁からデータをダウンロードします。
ダウンロードしたCSVはmacのExcelで開くと文字化けしたので、テキストエディタでUTF8(BOMあり)に文字コードを変換しておきました。
見せたい項目に編集したCSVデータです。
weather_data.csv
年月日,曜日,平均気温(℃),降水量の合計(mm) 2022/11/08,火,15.9,0 2022/11/09,水,14.6,0 2022/11/10,木,15.2,0 2022/11/11,金,16.1,0 2022/11/12,土,16.6,0 2022/11/13,日,18,0.5 2022/11/14,月,16.1,0 2022/11/15,火,11.1,6 2022/11/16,水,12.4,0 2022/11/17,木,12.8,0 2022/11/18,金,13.2,0 2022/11/19,土,13.7,0 2022/11/20,日,11.6,6 2022/11/21,月,13.4,3 2022/11/22,火,15.4,0 2022/11/23,水,11.7,42 2022/11/24,木,15.3,0.5 2022/11/25,金,13.5,0.5 2022/11/26,土,12.7,1 2022/11/27,日,14.4,0 2022/11/28,月,11.7,0 2022/11/29,火,16.7,10.5 2022/11/30,水,16.6,32.5 2022/12/01,木,10.1,0.5 2022/12/02,金,9.4,0 2022/12/03,土,7.8,0 2022/12/04,日,10.2,0 2022/12/05,月,7.6,19.5 2022/12/06,火,6.1,9.5 2022/12/07,水,8.5,0 2022/12/08,木,9.3,0
読み込み用のModel作成
グラフのModel
struct DailyWeather: Identifiable { var id = UUID() /// 年月日 2022/11/09 var yearMonthDay: String /// 曜日 var weekday: String /// 日時 var date: Date /// 平均気温 var averageTemperature: Double /// 降水量の合計 var precipitationTotal: Double }
CSVを読み込むローダー
class DailyWeatherLoader { func load() async -> [DailyWeather]? { guard let url = Bundle.main.url(forResource: "weather_data", withExtension: "csv") else { return nil } var dailyWeathers: [DailyWeather]? do { let data = try Data(contentsOf: url) let csvStr = String(data: data, encoding: .utf8) // 行ごとに分解 guard let lines = csvStr?.split(separator: "\r\n").map({ String($0) }) else { return nil } // 列ごとに分解 dailyWeathers = lines.map { line in let columns = line.split(separator: ",").map({ String($0) }) let yearMonthDay = columns[0] let weekday = columns[1] let formatter = DateFormatter() formatter.locale = Locale(identifier: "ja") formatter.timeZone = TimeZone(identifier: "Asia/Tokyo") formatter.dateFormat = "yyyy/MM/dd" guard let date = formatter.date(from: yearMonthDay ), let averageTemperature = Double(columns[2]), let precipitationTotal = Double(columns[3]) else { return nil } return DailyWeather(yearMonthDay: yearMonthDay, weekday: weekday, date: date, averageTemperature: averageTemperature, precipitationTotal: precipitationTotal) }.compactMap({ $0 }) } catch { return nil } return dailyWeathers } }
アプリ用のViewModel
class WeatherModel: ObservableObject { @Published var dailyWeathers: [DailyWeather] = [] func loadWeather() { let loader = DailyWeatherLoader() Task { let result = await loader.load() if let dailyWeathers = result { Task.detached { @MainActor [weak self] in self?.dailyWeathers = dailyWeathers } } } } }
降水量の合計の棒グラフ
降水量の合計の棒グラフを作ってみます
import SwiftUI import Charts struct ContentView: View { @StateObject var weatherModel: WeatherModel = WeatherModel() var body: some View { Chart { ForEach(weatherModel.dailyWeathers) { weather in BarMark( x: .value("date", weather.date), y: .value("precipitationTotal", weather.precipitationTotal) ) } } .chartYScale(domain: 0 ... 50) .chartXAxis { AxisMarks(position: .bottom, values: .stride(by: .day, count: 2)) { value in AxisValueLabel() { if let weather = weatherModel.dailyWeathers[value.index] { Text(weather.yearMonthDay).font(.system(size: 8)) .frame(width: 50, height: 30, alignment: .bottom) .offset(x: -10, y: -20) .rotationEffect(.degrees(-90)) } } } } .chartYAxisLabel("降水量の合計(mm)", position: .trailing, alignment: .center, spacing: 0) .chartXAxisLabel("年月日", position: .bottom, alignment: .top, spacing: 30) .padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)) .onAppear(perform: { weatherModel.loadWeather() }) } }
平均気温の折れ線グラフ
平均気温の折れ線グラフを作ってみます
import SwiftUI import Charts struct ContentView: View { @StateObject var weatherModel: WeatherModel = WeatherModel() var body: some View { Chart { ForEach(weatherModel.dailyWeathers) { weather in LineMark( x: .value("date", weather.date), y: .value("averageTemperature", weather.averageTemperature) ) } } .chartXAxis { AxisMarks(position: .bottom, values: .stride(by: .day, count: 2)) { value in AxisValueLabel() { if let weather = weatherModel.dailyWeathers[value.index] { Text(weather.yearMonthDay).font(.system(size: 8)) .frame(width: 50, height: 30, alignment: .leading) .offset(x: -10, y: -20) .rotationEffect(.degrees(-90)) } } } } .chartYAxisLabel("平均気温(℃)", position: .trailing, alignment: .center, spacing: 0) .chartXAxisLabel("年月日", position: .bottom, alignment: .top, spacing: 30) .padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)) .onAppear(perform: { weatherModel.loadWeather() }) } }
X軸の刻みの表示がちょっとおかしいのですが、すみません!
参考になれば幸いです。